/*
This file is part of [ahttpserver] project. 

Author: Artem Kustikov (kustikoff[at]tut.by)


This code is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any
damages arising from the use of this code.

Permission is granted to anyone to use this code for any
purpose, including commercial applications, and to alter it and
redistribute it freely, subject to the following restrictions:

1. The origin of this code must not be misrepresented; you must
not claim that you wrote the original code. If you use this
code in a product, an acknowledgment in the product documentation
would be appreciated but is not required.

2. Altered source versions must be plainly marked as such, and
must not be misrepresented as being the original code.

3. This notice may not be removed or altered from any source
distribution.
*/

#include "aconnect/config.hpp"
#include "aconnect/boost_format_safe.hpp"

#include <iostream>
#include <assert.h>

#include <boost/filesystem.hpp>
#include <boost/thread.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/timer.hpp>

#include "settings_loader.hpp"

#if defined (WIN32)
#	include <signal.h>
#elif defined (__GNUC__)
#	include <sys/signal.h>
#endif  //__GNUC__

// aconnect
#include "ahttplib.hpp"
#include "aconnect/util.hpp"
#include "aconnect/util.string.hpp"
#include "aconnect/util.file.hpp"
#include "aconnect/util.guards.hpp"

#include "constants.hpp"



namespace algo = boost::algorithm;
namespace fs = boost::filesystem;

// globals
namespace Global
{
	fs::path appPath;
	SettingsLoader settingsLoader;
	
	ahttp::HttpServerSettings globalSettings;
	ahttp::HttpServer httpServer;

	aconnect::BackgroundFileLogger logger;
	aconnect::Server commandServer;
	
	bool Stopped = false;
	int ReturnCode = ReturnCodes::Success;

	aconnect::Initializer SocketsLibInitializer (false);
}
////////////////////////////////////////////////////////////////////////
//
//	declarations
//
void init (aconnect::string_constptr relativeAppPath);
void destroy (bool waitWorkers = false);
void initSettingsLoader ();
void initLogger ();
void loadSettings (bool firstLoad);

void initHandlers ();
void initModules ();
void processExceptionAndExit (aconnect::string_constptr message, int exitCode);
void processSignal (int sig); 
void forceStop (); 

aconnect::socket_type findRunningServer();
void processCommand (const aconnect::ClientInfo& client);
void sendCommand (const aconnect::socket_type sock, aconnect::string_constref command);
void processServerCommandSend (aconnect::string command);

////////////////////////////////////////////////////////////////////////
//
//	definitions
//
void init (aconnect::string_constptr relativeAppPath) 
{
	signal (SIGINT, processSignal); 
	signal (SIGTERM, processSignal);
	// run-time errors processing
	signal (SIGSEGV, processSignal);
	signal (SIGFPE, processSignal);
	signal (SIGILL, processSignal);
	signal (SIGABRT, processSignal); 

#ifdef WIN32
	signal (SIGBREAK, processSignal);
#endif

	try {
		Global::appPath = aconnect::util::getAppLocation (relativeAppPath);
		
	} catch (std::exception &err) {
		processExceptionAndExit (err.what(), ReturnCodes::InitizalizationFailed);
	}

    // init socket library
	try {
		Global::SocketsLibInitializer.init();
	} catch (std::exception &ex) {
		processExceptionAndExit (ex.what(), ReturnCodes::InitizalizationFailed);
	}
}

void destroy (bool waitWorkers) 
{
	Global::logger.info ( "Stopping server..." );
    Global::Stopped = true;

	try {
		// force stop
		Global::httpServer.Engine.stop(waitWorkers);

		Global::logger.info ( "Destroy handlers..." );
		Global::globalSettings.destroyPlugins (ahttp::PluginHandler);
		Global::logger.info ( "Handlers destroyed" );

		Global::logger.info ( "Destroy modules..." );
		Global::globalSettings.destroyPlugins (ahttp::PluginModule);
		Global::logger.info ( "Modules destroyed" );
        
	} catch (std::exception &ex) {
		std::cerr << ex.what() << std::endl;
		Global::logger.critical(ex);
	}
    
	// close logger
    Global::logger.info ( "Server stopped" );
    Global::logger.destroy ();
}


void loadSettings (bool firstLoad)
{
	try {
		Global::settingsLoader.load (Global::globalSettings, firstLoad);
		
	} catch (std::exception &ex) {
		processExceptionAndExit (ex.what(), ReturnCodes::SettingsLoadFailed);
	
	} catch (...) {
		processExceptionAndExit ("Unknown exception caught at settings loading", ReturnCodes::SettingsLoadFailed);
	}
}

void initHandlers ()
{
	try 
	{
		Global::globalSettings.initPlugins(ahttp::PluginHandler, &Global::httpServer);
	
	} catch (std::exception &ex) {
		processExceptionAndExit (ex.what(), ReturnCodes::SettingsLoadFailed);
	
	} catch (...) {
		processExceptionAndExit ("Unknown exception caught at handlers intialization", ReturnCodes::SettingsLoadFailed);
	}
}

void initModules ()
{
	try 
	{
		Global::globalSettings.initPlugins(ahttp::PluginModule, &Global::httpServer);
	
	} catch (std::exception &ex) {
		processExceptionAndExit (ex.what(), ReturnCodes::SettingsLoadFailed);
	
	} catch (...) {
		processExceptionAndExit ("Unknown exception caught at modules intialization", ReturnCodes::SettingsLoadFailed);
	}
}

void initLogger () {
	using namespace aconnect;

	try {

		// init logger
		string logFileTemplate = Global::settingsLoader.logFileTemplate();
		Global::globalSettings.updateAppLocationInPath (logFileTemplate);
		
		fs::path logFilesDir = fs::path (logFileTemplate, fs::native).branch_path();
		if (!fs::exists (logFilesDir))
			fs::create_directories(logFilesDir);

		Global::logger.init (Global::settingsLoader.logLevel(), 
			logFileTemplate.c_str(), 
			Global::settingsLoader.maxLogFileSize());

		Global::logger.info ( "Server started" );

	} catch (std::exception &ex) {
		processExceptionAndExit (ex.what(), ReturnCodes::LoggerSetupFailed);
	
	} catch (...) {
		processExceptionAndExit ("Unknown exception caught at logger initialization", ReturnCodes::LoggerSetupFailed);
	}

}

void processExceptionAndExit (aconnect::string_constptr message, int exitCode) 
{
	std::cerr << "Unrecorable error caught: " << message << std::endl;
	Global::logger.critical ("Exception occurred: %s", message);
	Global::logger.destroy ();

	exit (exitCode);
}


void processSignal (int sig) {

	if (Global::Stopped)
	{
#if defined(WIN32) && !defined(_DEBUG)
		//if (sig == SIGSEGV)
		//	abort ();
#endif
			return;
	}
	
	Global::logger.critical ("Server received signal: %d",sig);

	destroy ();

	exit (ReturnCodes::ForceStopped);
}

void forceStop () {
	if (Global::Stopped)
		return;

	Global::logger.info ("Server stopping begin");
	
	destroy ();
	exit (ReturnCodes::ForceStopped);
}

aconnect::socket_type findRunningServer() 
{
	using namespace aconnect;
	socket_type clientSock = util::createSocket (AF_INET);
	
	struct sockaddr_in local;
	util::zeroMemory (&local, sizeof( local ));

	local.sin_family = AF_INET;
	local.sin_port = htons ( Global::globalSettings.commandPort() );
	local.sin_addr.s_addr = inet_addr ( "127.0.0.1" );

	int connectRes = connect (clientSock, (struct sockaddr*) &local, sizeof( local ) );
	if ( connectRes != 0 )
		clientSock = INVALID_SOCKET;
	
	return clientSock;
}



void processCommand (const aconnect::ClientInfo& client)
{
	using namespace aconnect;

	string command = Settings::CommandUnknown, response;
	bool stopServer = false;
	bool startNewServer = false;
	
	try
	{
		string endMark = Settings::EndMark;

		EndMarkSocketStateCheck check (endMark);
		command = client.getRequest (check);
		algo::erase_tail (command, (int) endMark.length());

		Global::logger.info("[server] command received: \"%s\"", command.c_str() );
		
		if (util::equals (command, Settings::CommandStop) || util::equals (command, Settings::CommandRestart)) {
			response = "Stopped";
			stopServer = true;

			if (util::equals (command, Settings::CommandRestart))
				startNewServer = true;
			
		} else if (util::equals (command, Settings::CommandStat)) {
			const int buffSize = 1024;
			char_type buff[buffSize];

			int formattedCount = snprintf (buff, buffSize, 
				Settings::StatisticsFormat,
				(long) Global::httpServer.RequestsCount,
				(long) Global::httpServer.Engine.currentWorkersCount(),
				(long) Global::httpServer.Engine.currentPendingWorkersCount(),
				(size_t) Global::httpServer.Engine.requestsQueueLength());
			
			response.append (buff, util::min2(formattedCount, buffSize));
		
		} else if (util::equals (command, Settings::CommandReload)) {

			response = "Directories settings reloaded";
			try 
			{
				Global::Stopped = true;

				Global::httpServer.Engine.stop (true);
				Global::globalSettings.destroyPlugins (ahttp::PluginHandler, false);
				Global::globalSettings.destroyPlugins (ahttp::PluginModule, false);
				
				Global::Stopped = false;

				Global::settingsLoader.loadFile(Global::settingsLoader.configFilePath());
				Global::settingsLoader.load ( Global::globalSettings, false );

				Global::globalSettings.initPlugins(ahttp::PluginModule, &Global::httpServer);
				Global::globalSettings.initPlugins(ahttp::PluginHandler, &Global::httpServer);
				
				Global::httpServer.Engine.start();

			} catch (ahttp::settings_load_error &ex) {
				response = string ("Settings reload failed: ") + ex.what();

				stopServer = true;
				Global::ReturnCode = ReturnCodes::SettingsLoadFailed;
			
			} catch (...) {
				response = "Settings reload failed: unknown error";

				stopServer = true;
				Global::ReturnCode = ReturnCodes::SettingsLoadFailed;
			}


			
		} else {
			response = "Unknown command: " + command;
		}

		Global::logger.info("[server] command \"%s\" processed, response: %s", 
			command.c_str(),
			response.c_str());

		try {
			client.writeResponse(response + endMark);
		
		} catch (std::exception &ex) {
			Global::logger.warn("Command response sending failed, command: %s, error: %s",
				command.c_str(), ex.what() );
		}

		if (stopServer)   
		{
			destroy (true);
			client.close();
			
			Global::commandServer.stop(false);

			if (startNewServer)
				util::startProcess (Global::appPath.file_string().c_str(), Settings::CommandStart);
		}
		
	}
	catch (std::exception &ex)
	{
		Global::logger.critical("Command processing failed, command: %s, error: %s",
			command.c_str(), ex.what() );
	}
}

void sendCommand (const aconnect::socket_type sock, aconnect::string_constref command)
{
	using namespace aconnect;
	try
	{
		string endMark = Settings::EndMark;
		util::writeToSocket(sock, command + endMark);

		EndMarkSocketStateCheck check (endMark);
		string response = util::readFromSocket(sock, check);
		
		algo::erase_tail (response, (int) endMark.length());
		std::cout << response << std::endl;
	}
	catch (std::exception &ex)
	{
		std::cout << "Command sending failed: " << ex.what() << std::endl;
	}
}

void processServerCommandSend (aconnect::string command)
{
	using namespace aconnect;
	// find running server
	socket_type sock = findRunningServer ();

	if (util::equals(command, Settings::CommandStart))
	{
		if (sock != INVALID_SOCKET) {
			std::cerr << "Server already started - 'stat' command will be sent" << std::endl;
			command = Settings::CommandStat;

		} else {
#if defined (WIN32)
		
			try {
				util::startProcess (Global::appPath.file_string().c_str(), Settings::CommandRun);
			
			}  catch (std::exception &ex) {
					std::cerr << "Server startup failed: " << ex.what() << std::endl;
			}
#else
			std::cerr << "Wrong execution path - 'start' command cannot be processed" << std::endl;
#endif
			return;
		}
	}

	// write commands list
	std::cout << Settings::CommandsList << std::endl
		<< Settings::BreakLine << std::endl;

	if (sock != INVALID_SOCKET) {
		sendCommand (sock, command);
		util::closeSocket (sock);
	
	} else {
		std::cerr << "Server is not started" << std::endl;
	}
}

void initSettingsLoader ()
{
	try 
	{
		aconnect::string appLocation = fs::path(Global::appPath).remove_leaf().directory_string().c_str();

		Global::globalSettings.AppLocation = appLocation;
		
		fs::path configFilePath = fs::complete (DefaultFiles::ConfigFileName, Global::appPath.branch_path());
		aconnect::string settingsFilePath = configFilePath.file_string();

		if ( settingsFilePath.empty())
			processExceptionAndExit ("Empty settings file path to load", ReturnCodes::InitizalizationFailed); 

		if ( !fs::exists(configFilePath))
			processExceptionAndExit ("Settings file does not exist", ReturnCodes::InitizalizationFailed); 

		Global::settingsLoader.loadFile (settingsFilePath);

		Global::settingsLoader.loadLoggerSettings();
		
	} catch (std::exception &ex) {
		processExceptionAndExit (ex.what(), ReturnCodes::SettingsLoadFailed);
	
	} catch (...) {
		processExceptionAndExit ("Unknown exception caught at settings loader intialization", ReturnCodes::SettingsLoadFailed);
	}
}

inline double getTimerValue (boost::timer &timer)
{
	double res = timer.elapsed(); 
	timer.restart();

	return res;
}

//////////////////////////////////////////////////////////////////////////
//		Entry point
//////////////////////////////////////////////////////////////////////////
int main (int argc, char* args[]) 
{
	using namespace aconnect;
	
	init (args[0]);
	
	boost::timer loadTimer;

	double settingsFileLoadTime = 0,
		loggerInitTime = 0,
		serverInitTime = 0,
		settingsLoadTime = 0,
		modulesInitTime = 0,
		handlersInitTime = 0,
		serverStartupTime = 0;
	
	Global::logger.setUseStderrTillInit();

	initSettingsLoader ();
	settingsFileLoadTime = getTimerValue(loadTimer);

	loadSettings (true);
	settingsLoadTime = getTimerValue(loadTimer);
	
	string command = Settings::CommandStat;
	if (argc > 1) 
		command = args[1];
	
#if defined (WIN32)
	if (!util::equals (Settings::CommandRun, command)) 
	{
#else
	if (!util::equals (Settings::CommandRun, command) && 
		!util::equals (Settings::CommandStart, command)) 
	{
#endif
		processServerCommandSend (command);
		destroy();
		return ReturnCodes::Success;
	}
    
	// start server
	util::scoped_guard<util::simple_callback>  guard (forceStop);
	
	Global::globalSettings.setLogger ( &Global::logger);
	
	// init HTTP server (in child thread)
	Global::httpServer.init (&Global::globalSettings);
	Global::httpServer.Engine.setStopProcessProc (forceStop);

	serverInitTime = getTimerValue(loadTimer);

	initModules ();
	modulesInitTime = getTimerValue(loadTimer);

	initHandlers ();
	handlersInitTime = getTimerValue(loadTimer);
		
	// init command server
	ServerSettings cmdServerSettings;
	cmdServerSettings.socketReadTimeout = 
		cmdServerSettings.socketWriteTimeout = Global::globalSettings.CommandSocketTimeout;
	
	cmdServerSettings.enablePooling = false;

	Global::commandServer.setLog ( &Global::logger);
	Global::commandServer.init (Global::globalSettings.commandPort(), 
		processCommand,
		cmdServerSettings);

	try 
	{

#if !defined (WIN32)
		if (util::equals (Settings::CommandStart, command))
#endif
		util::detachFromConsole ();	
	
	    initLogger ();
		loggerInitTime = getTimerValue(loadTimer);


		Global::httpServer.Engine.start();
		serverStartupTime = getTimerValue(loadTimer);

		// write timing
		boost::format record ("%s: elapsed time - %f sec");

		record.clear(); record % "Settings file load" % settingsFileLoadTime;
		Global::logger.info(record.str().c_str());
		
		record.clear(); record % "Logger initialization" % loggerInitTime;
		Global::logger.info(record.str().c_str());

		record.clear(); record % "Settings load" % settingsLoadTime;
		Global::logger.info(record.str().c_str());
		
		record.clear(); record % "HTTP server initialization" % serverInitTime;
		Global::logger.info(record.str().c_str());
		
		record.clear(); record % "Modules initialization" % modulesInitTime;
		Global::logger.info(record.str().c_str());

		record.clear(); record % "Handlers initialization" % handlersInitTime;
		Global::logger.info(record.str().c_str());

		record.clear(); record % "Server startup" % serverStartupTime;
		Global::logger.info(record.str().c_str());

		// start command retrieving cycle
		Global::commandServer.start (true);

	} catch (std::exception &ex)  {

		#if defined (WIN32)
		::MessageBoxA(NULL, ex.what(), Global::globalSettings.serverVersion().c_str(), MB_OK | MB_ICONERROR);	
		#endif

		Global::logger.critical ("Exception caught at server startup (%s): %s", 
			typeid(ex).name(), ex.what());
		
		std::cerr << "Server startup failed: " << typeid(ex).name() <<  ex.what() << std::endl;

		destroy ();
		
		Global::commandServer.stop();

		return ReturnCodes::ServerStartupFailed;
	}

	return Global::ReturnCode;
}
